package com.myk.game.gobangchess.utils;

import com.myk.game.gobangchess.constants.PathCost;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 自定义的日志系统
 */
public class LogUtils {

    private static final Config CONFIG = new Config(); //全局配置信息
    private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(); //用于输入本地文件日志的线程
    private static final String SPACE = " ";
    private static final String SPACE_x_2 = "  ";
    private static final String SPACE_x_4 = "    ";
    private static final String BRACKET_LEFT = "[";
    private static final String BRACKET_RIGHT = "]";
    private static final String FROM_ARROW = "-> ";
    private static final String LINE_SEP = System.lineSeparator();

    private static SimpleDateFormat sDateFormat;
    private static SimpleDateFormat sTimeFormat;

    //获取日志日期格式
    private static SimpleDateFormat getsDateFormat() {
        return sDateFormat == null ? new SimpleDateFormat("yyyy_MM_dd", Locale.CHINA) : sDateFormat;
    }

    //获取日志时间格式
    private static SimpleDateFormat getsTimeFormat() {
        return sTimeFormat == null ? new SimpleDateFormat("HH:mm:ss.SSS", Locale.CHINA) : sTimeFormat;
    }

    //判断文件名称是否符合日志格式
    private static boolean isMatchLogFileName(String name) {
        return name.matches("^" + CONFIG.getLogFilePrefix() + "_[0-9]{4}_[0-9]{2}_[0-9]{2}.*$");
    }

    //获取日志文件名中的日期文本
    private static String findLogDate(String name) {
        Pattern pattern = Pattern.compile("[0-9]{4}_[0-9]{2}_[0-9]{2}");
        Matcher matcher = pattern.matcher(name);
        if (matcher.find()) {
            return matcher.group();
        }
        return "";
    }

    //获取日志线程名称和方法调用栈信息
    private static String getLogStack() {
        String treadName = Thread.currentThread().getName();
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        int stackIndex = 3 + CONFIG.getStackOffset(); //这边这个3是考虑到LogUtils自身经历了几层调用，这几层没有打印的意义。
        int stackDeep = CONFIG.getStackDeep();
        if (stackDeep <= 1) {
            if (stackIndex >= stackTrace.length || stackIndex < 0) { //索引超出范围了
                return BRACKET_LEFT + treadName + BRACKET_RIGHT;
            } else {
                StackTraceElement targetElement = stackTrace[stackIndex];
                return BRACKET_LEFT + treadName + ", " + targetElement.toString() + BRACKET_RIGHT;
            }
        } else {
            StringBuilder stackArrStr = new StringBuilder();
            for (int i = 0; i < stackDeep; i++) {
                int index = i + stackIndex;
                if (index < stackTrace.length && stackIndex >= 0) {
                    if (i == 0) stackArrStr.append(stackTrace[index].toString());
                    else
                        stackArrStr.append(LINE_SEP).append(SPACE_x_4).append(FROM_ARROW).append(stackTrace[index].toString());
                }
            }
            return BRACKET_LEFT + treadName + ", " + stackArrStr + BRACKET_RIGHT;
        }
    }

    //打印新建的日志文件的顶部信息
    private static void printDeviceInfo(String date, String filePath) {
        String headContent = "" +
                "******************** Log Head ********************" + LINE_SEP +
                "                Date : " + date + LINE_SEP +
                "******************** Log Head ********************" + LINE_SEP;
        FileUtils.writeFileFromString(filePath, headContent, true);
    }

    //打印日志到控制台
    private static void print2Console(String content) {
        System.out.println(content);
    }

    //打印日志到本地文件
    private static void print2File(String date, String content) {
        String curLogFilePath = getLogPathByDate(date);
        if (createOrExistsLogFile(date, curLogFilePath)) {
            FileUtils.writeFileFromString(curLogFilePath, LINE_SEP + content, true);
        }
    }

    //初始化日志文件
    private static boolean createOrExistsLogFile(String date, String filePath) {
        File file = new File(filePath);
        if (file.exists()) return file.isFile();
        if (!FileUtils.createOrExistsDir(file.getParentFile())) return false;
        try {
            deleteDueLogs(filePath, date);
            boolean isCreate = file.createNewFile();
            if (isCreate) {
                printDeviceInfo(date, filePath);
            }
            return isCreate;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

    //删除过期日志文件
    private static void deleteDueLogs(final String filePath, final String date) {
        if (CONFIG.getLogSaveDays() <= 0) return;
        File file = new File(filePath);
        File parentFile = file.getParentFile();
        File[] files = parentFile.listFiles((dir, name) -> isMatchLogFileName(name));
        if (files == null || files.length <= 0) return;
        try {
            SimpleDateFormat dateFormat = getsDateFormat();
            long dueMillis = dateFormat.parse(date).getTime() - CONFIG.getLogSaveDays() * 86400000L;  //86400000等于一天的秒数
            for (final File aFile : files) {
                String name = aFile.getName();
                String logDay = findLogDate(name);
                if (dateFormat.parse(logDay).getTime() <= dueMillis) {
                    EXECUTOR.execute(aFile::delete);
                }
            }
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    /**
     * 打印日志
     */
    public static void log(String text) {
        if (!CONFIG.getLogSwitch()) return;

        String consoleContentStr = text;
        String fileContentStr = text;
        String dateStr = "";
        if (CONFIG.isPrintStack) {
            String staceStr = getLogStack();
            if (CONFIG.isLog2Console) {
                consoleContentStr = staceStr + LINE_SEP + SPACE_x_4 + consoleContentStr;
            }
            if (CONFIG.isLog2File) {
                fileContentStr = staceStr + LINE_SEP + SPACE_x_4 + fileContentStr;
            }
        }
        Date date = new Date();
        dateStr = getsDateFormat().format(date);
        String timeStr = getsTimeFormat().format(date);
        if (CONFIG.isLog2Console) {
            consoleContentStr = dateStr + SPACE + timeStr + SPACE_x_2 + consoleContentStr;
        }
        if (CONFIG.isLog2File) {
            fileContentStr = timeStr + SPACE_x_2 + fileContentStr;
        }

        if (CONFIG.isLog2Console) {
            print2Console(consoleContentStr);
        }
        if (CONFIG.isLog2File) {
            String fileContentStrF = fileContentStr;
            String dateStrF = dateStr;
            EXECUTOR.execute(() -> print2File(dateStrF, fileContentStrF));
        }
    }

    /**
     * 仅打印到控制台，不论Config如何设置
     */
    public static void log2Console(String text) {
        String consoleContentStr = text;
        if (CONFIG.isPrintStack) {
            String staceStr = getLogStack();
            consoleContentStr = staceStr + LINE_SEP + SPACE_x_4 + consoleContentStr;
        }
        Date date = new Date();
        String timeStr = getsTimeFormat().format(date);
        String dateStr = getsDateFormat().format(date);
        consoleContentStr = dateStr + SPACE + timeStr + SPACE_x_4 + consoleContentStr;
        print2Console(consoleContentStr);
    }

    /**
     * 仅打印到日志文件，不论Config如何设置
     */
    public static void log2File(String text) {
        String fileContentStr = text;
        String dateStr = "";
        if (CONFIG.isPrintStack) {
            String staceStr = getLogStack();
            fileContentStr = staceStr + LINE_SEP + SPACE_x_4 + fileContentStr;
        }
        Date date = new Date();
        dateStr = getsDateFormat().format(date);
        String timeStr = getsTimeFormat().format(date);
        fileContentStr = timeStr + SPACE_x_2 + fileContentStr;
        String fileContentStrF = fileContentStr;
        String dateStrF = dateStr;
        EXECUTOR.execute(() -> print2File(dateStrF, fileContentStrF));
    }

    /**
     * 获取对应日期的日志文件路径
     */
    public static String getLogPathByDate(String dateStr) {
        return CONFIG.getLogDir() +
                CONFIG.getLogFilePrefix() +
                "_" + dateStr +
                CONFIG.getLogFileExtension();
    }

    /**
     * 获取当前日志文件
     */
    public static File getNowLogFile() {
        return new File(getLogPathByDate(getsDateFormat().format(new Date())));
    }

    /**
     * 获取全部日志文件
     */
    public static List<File> getLogFiles() {
        String dir = CONFIG.getLogDir();
        File logDir = new File(dir);
        if (!logDir.exists()) return new ArrayList<>();
        File[] files = logDir.listFiles((dir1, name) -> isMatchLogFileName(name));
        List<File> list = new ArrayList<>();
        Collections.addAll(list, files);
        return list;
    }

    /**
     * 获取全局的配置信息
     */
    public static Config getConfig() {
        return CONFIG;
    }

    /**
     * 全局配置信息
     */
    public static class Config {

        private boolean isLog2Console = true; //日志是否打印到控制台
        private boolean isLog2File = false; //日志是否保存到文件
        private boolean isPrintStack = true; //是否打印调用栈信息
        private int mStackDeep = 1; //日志打印方法调用栈深度
        private int mStackOffset = 0;   //日志打印方法调用栈偏移度

        private final String mDefaultDir; //日志文件默认存储路径
        private String logDir; //日志文件存储路径
        private String logFilePrefix = "logutils"; //日志文件前缀
        private final String logFileExtension; //日志文件扩展名
        private int logSaveDays = -1; //日志文件存储天数

        private Config() {
            mDefaultDir = PathCost.LOGS_DIR;
            logFileExtension = ".txt";
        }

        public boolean getLogSwitch() {
            return isLog2Console || isLog2File;
        }

        public boolean isLog2Console() {
            return isLog2Console;
        }

        public boolean isLog2File() {
            return isLog2File;
        }

        public boolean isPrintStack() {
            return isPrintStack;
        }

        public String getDefaultDir() {
            return mDefaultDir;
        }

        public final String getLogDir() {
            return logDir == null ? mDefaultDir : logDir;
        }

        public final String getLogFilePrefix() {
            return logFilePrefix;
        }

        public final String getLogFileExtension() {
            return logFileExtension;
        }

        public final int getLogSaveDays() {
            return logSaveDays;
        }

        public int getStackDeep() {
            return mStackDeep;
        }

        public int getStackOffset() {
            return mStackOffset;
        }

        public Config setLog2Console(boolean log2Console) {
            isLog2Console = log2Console;
            return this;
        }

        public Config setLog2File(boolean log2File) {
            isLog2File = log2File;
            return this;
        }

        public Config setPrintStack(boolean printStack) {
            isPrintStack = printStack;
            return this;
        }

        public Config setLogDir(String dir) {
            this.logDir = dir.endsWith("/") ? dir : dir + "/";
            return this;
        }

        public Config setLogDir(File dir) {
            this.logDir = dir.getAbsolutePath() + "/";
            return this;
        }

        public Config setLogFilePrefix(String logFilePrefix) {
            if (StringUtils.isSpace(logFilePrefix)) {
                this.logFilePrefix = "logutils";
            } else {
                this.logFilePrefix = logFilePrefix;
            }
            return this;
        }

        /**
         * 日志保存天数，负数为永久保存，最好不要输入0（边写边删没意义）。
         */
        public Config setLogSaveDays(int logSaveDays) {
            this.logSaveDays = logSaveDays;
            return this;
        }

        /**
         * 设置打印的方法调用栈深度，输入大于等于1的数
         */
        public Config setStackDeep(int mStackDeep) {
            this.mStackDeep = mStackDeep;
            return this;
        }

        /**
         * 设置打印方法调用栈的偏移，输入大于0的数
         */
        public Config setStackOffset(int mStackOffset) {
            this.mStackOffset = mStackOffset;
            return this;
        }
    }
}
